1-6 JWT 依赖安装 & JwtModule 工作原理解析
本节从知识脉络回顾出发,安装 JWT 所需依赖,配置 AuthModule 中的 JwtModule,创建 JwtStrategy 策略文件,并深入源码追踪 validate() 方法的调用链路。
知识脉络回顾:Pipe → Guard → JWT 的学习路径
用户认证需求
├── 1. 接收用户数据 → 需要参数校验 → 学习 Pipe(管道)
│ ├── ValidationPipe — 校验请求数据
│ └── 自定义管道 — 数据转换
│
├── 2. 保护路由接口 → 需要访问控制 → 学习 Guard(守卫)
│ └── AuthGuard('jwt') — 拦截未认证请求
│
└── 3. 签发身份凭证 → 需要加密签名 → 学习 JWT(令牌)
├── JwtModule — 配置密钥和过期时间
├── JwtService.signAsync() — 签发 Token
└── JwtStrategy.validate() — 验证 Token
text
Pipe 是学习 Guard 和 JWT 前的必要前置知识,解决"请求数据如何正确到达 Controller"的问题。
JWT 认证完整工作流程
┌───────────────────── 阶段一:登录 ──────────────────────┐
│ │
│ 前端 POST /auth/signin { username, password } │
│ │ │
│ ▼ │
│ Pipe 校验(ValidationPipe + DTO) │
│ │ │
│ ▼ 校验通过 │
│ AuthController(公开接口,无需 Token) │
│ │ │
│ ▼ │
│ AuthService → UserRepository → 数据库 │
│ │ │
│ ▼ 用户存在且密码正确 │
│ JwtService.signAsync({ sub: userId, username }) │
│ │ 使用服务器密钥签名 │
│ ▼ │
│ 响应 { access_token: "eyJhbG..." } │
│ │
└─────────────────────────────────────────────────────────┘
┌───────────────────── 阶段二:访问受保护接口 ─────────────┐
│ │
│ 前端 GET /auth/profile │
│ Headers: Authorization: Bearer eyJhbG... │
│ │ │
│ ▼ │
│ Guard 拦截(AuthGuard('jwt')) │
│ │ │
│ ▼ │
│ JwtStrategy 自动验证: │
│ 1. 提取 Bearer Token │
│ 2. 使用 secret 验证签名 │
│ 3. 检查过期时间 │
│ 4. 解码 payload │
│ │ │
│ ▼ │
│ JwtStrategy.validate(payload) → req.user │
│ │ │
│ ▼ │
│ Controller 处理请求,返回 req.user │
│ │
└─────────────────────────────────────────────────────────┘
text
关键约定:
- Token 必须放在请求头
Authorization: Bearer <token>中 - 不能放在 body 或 query 参数中(passport-jwt 默认校验的是 headers)
Authorization属性名不可变更
安装依赖
# Passport 核心
pnpm add passport
pnpm add -D @types/passport
# NestJS JWT 模块
pnpm add @nestjs/jwt
# Passport JWT 策略
pnpm add passport-jwt
pnpm add -D @types/passport-jwt
# NestJS Passport 桥接模块
pnpm add @nestjs/passport
bash
依赖版本参考(大版本号一致即可):
| 包名 | 版本 |
|---|---|
@nestjs/jwt | v10.x |
@nestjs/passport | 跟随 NestJS 主版本 |
passport | v0.7.x |
passport-jwt | v4.x |
配置 JWT 密钥(环境变量)
在 .env 文件中配置 JWT 密钥:
# .env
JWT_SECRET=aB3dE7fG9hJ2kL5mN8pQ1rS4tU6vW0xYzAbCdEfGhIjKlMnOpQrStUvWxYz123456
bash
密钥应使用 64 位以上的随机字符串,可通过在线密码生成器生成。生产环境中密钥必须通过环境变量管理,不可硬编码。
配置 AuthModule
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategy/jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1d' },
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
typescript
JwtModule.registerAsync vs JwtModule.register:
| 方法 | 适用场景 | 密钥来源 |
|---|---|---|
register() | 密钥硬编码 | 直接写字符串 |
registerAsync() | 密钥来自环境变量 | 通过 ConfigService 读取 |
signOptions.expiresIn 建议值:
| 场景 | 建议值 | 说明 |
|---|---|---|
| 开发环境 | '7d' | 方便调试 |
| 一般应用 | '1d' 或 '8h' | 平衡安全与体验 |
| 电商平台 | '2h' ~ '4h' | 较短,配合 Refresh Token |
| 金融应用 | '15m' ~ '30m' | 最短,安全性最高 |
创建 JwtStrategy
在 auth/strategy/ 目录下创建策略文件:
// auth/strategy/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(protected configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: any) {
// payload 由 JwtStrategy 自动解码
// 包含签发时放入的 sub 和 username,以及自动添加的 iat 和 exp
return { userId: payload.sub, username: payload.username };
}
}
typescript
constructor中使用protected而非private,是因为super()调用需要先于属性初始化。ConfigService作为全局模块,在任何地方都可注入。
validate() 方法的调用链路追踪
validate() 方法在 TypeScript 中无法通过 IDE 自动提示找到,因为它定义在 @nestjs/passport 的抽象类中,调用链路较深:
@nestjs/passport 源码调用链:
═══════════════════════════
PassportStrategy(抽象类)
├── abstract validate(...args): any ← 抽象方法,由开发者实现
│
└── callback()
└── 调用 this.validate(...args) ← 框架在认证通过后回调
关键源码(简化版):
text
// @nestjs/passport/dist/passport/passport.strategy.js(简化)
export const PassportStrategy = (Strategy) => {
abstract class PassportStrategy extends Strategy {
// 抽象方法,由 JwtStrategy / LocalStrategy 实现
abstract validate(...args: any[]): any;
// Passport 认证成功后的回调
callback = (error, user, info, status) => {
// 调用开发者实现的 validate 方法
const result = this.validate(user, info);
return result;
};
}
return PassportStrategy;
};
typescript
调用时序:
1. 请求到达 → AuthGuard('jwt') 拦截
2. AuthGuard 调用 Passport 的 authenticate 方法
3. Passport 使用 passport-jwt 的 Strategy 验证 Token
4. 验证通过 → 触发 callback
5. callback 内部调用 JwtStrategy.validate(payload)
6. validate 返回值 → 赋值到 req.user
text
validate()不是passport或passport-jwt定义的,而是@nestjs/passport在 Passport 原生 callback API 上封装的统一接口。这使得所有策略的写法保持一致。
本节要点
- 学习路径:Pipe(参数校验)→ Guard(路由保护)→ JWT(身份凭证),三者层层递进
- 依赖安装:
@nestjs/jwt、@nestjs/passport、passport、passport-jwt及对应类型声明 - JwtModule 配置:使用
registerAsync+ConfigService从环境变量读取密钥 - 密钥安全:64 位随机字符串,通过
.env管理,不硬编码 - JwtStrategy:继承
PassportStrategy(Strategy),实现validate(payload)方法 - validate() 调用链:
@nestjs/passport在 Passport 原生 callback 上封装的抽象方法,IDE 无法自动提示但运行时正常调用
↑